Punto 2#

Librerías Utilizadas#

import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from statsmodels.stats.outliers_influence import variance_inflation_factor
from sklearn.preprocessing import OneHotEncoder
import pandas as pd
import numpy as np
from sklearn.preprocessing import OneHotEncoder
from scipy.stats import chi2_contingency
import plotly.express as px
import datetime
from statsmodels.tsa.seasonal import seasonal_decompose
import matplotlib.pyplot as plt
import os
import ssl
ssl._create_default_https_context = ssl._create_unverified_context

Wind Speed EDA#

Iniciamos cargando el dataset y observando las primeras filas.

wind = pd.read_csv("https://raw.githubusercontent.com/lihkir/Data/main/wind_speed/data_treino_dv_df_2000_2010.csv")
wind.head()
HORA (UTC) VENTO, DIRE��O HORARIA (gr) (� (gr)) VENTO, VELOCIDADE HORARIA (m/s) UMIDADE REL. MAX. NA HORA ANT. (AUT) (%) UMIDADE REL. MIN. NA HORA ANT. (AUT) (%) TEMPERATURA M�XIMA NA HORA ANT. (AUT) (�C) TEMPERATURA M�NIMA NA HORA ANT. (AUT) (�C) UMIDADE RELATIVA DO AR, HORARIA (%) PRESSAO ATMOSFERICA AO NIVEL DA ESTACAO, HORARIA (mB) PRECIPITA��O TOTAL, HOR�RIO (mm) VENTO, RAJADA MAXIMA (m/s) PRESS�O ATMOSFERICA MAX.NA HORA ANT. (AUT) (mB) PRESS�O ATMOSFERICA MIN. NA HORA ANT. (AUT) (mB)
0 12:00 0.809017 1.8 69.0 60.0 22.6 20.7 61.0 888.2 0.0 3.8 888.2 887.7
1 13:00 0.965926 2.7 62.0 55.0 24.2 22.5 55.0 888.4 0.0 4.7 888.4 888.2
2 14:00 0.891007 2.0 56.0 50.0 25.5 24.3 51.0 888.1 0.0 4.9 888.4 888.1
3 15:00 0.848048 2.5 52.0 44.0 27.4 25.0 44.0 887.4 0.0 5.8 888.1 887.4
4 16:00 0.224951 2.4 50.0 43.0 27.1 25.5 46.0 886.5 0.0 5.8 887.4 886.5

A primera vista se puede notar que las columnas tienen nombres muy largos y que se dificulta la interpretación de cada variable. También, se observa que todas las variables son numéricas, a excepción de la hora que es temporal. Veamos con más detalle los nombres de las columnas.

wind.columns
Index(['HORA (UTC)', 'VENTO, DIRE��O HORARIA (gr) (� (gr))',
       'VENTO, VELOCIDADE HORARIA (m/s)',
       'UMIDADE REL. MAX. NA HORA ANT. (AUT) (%)',
       'UMIDADE REL. MIN. NA HORA ANT. (AUT) (%)',
       'TEMPERATURA M�XIMA NA HORA ANT. (AUT) (�C)',
       'TEMPERATURA M�NIMA NA HORA ANT. (AUT) (�C)',
       'UMIDADE RELATIVA DO AR, HORARIA (%)',
       'PRESSAO ATMOSFERICA AO NIVEL DA ESTACAO, HORARIA (mB)',
       'PRECIPITA��O TOTAL, HOR�RIO (mm)', 'VENTO, RAJADA MAXIMA (m/s)',
       'PRESS�O ATMOSFERICA MAX.NA HORA ANT. (AUT) (mB)',
       'PRESS�O ATMOSFERICA MIN. NA HORA ANT. (AUT) (mB)'],
      dtype='object')

Procederemos a realizar un cambio en los nombres de las columnas puesto que se dificulta la lectura de estas. El dataset con las nombres de las variables cambiadas es el siguiente:

columnas = {'HORA (UTC)': 'hora', 'VENTO, DIRE��O HORARIA (gr) (� (gr))': 'direccion_viento', 
            'VENTO, VELOCIDADE HORARIA (m/s)': 'velocidad_viento', 'UMIDADE REL. MAX. NA HORA ANT. (AUT) (%)': 'humedad_max',
            'UMIDADE REL. MIN. NA HORA ANT. (AUT) (%)': 'humedad_min', 'TEMPERATURA M�XIMA NA HORA ANT. (AUT) (�C)': 'temperatura_max',
            'TEMPERATURA M�NIMA NA HORA ANT. (AUT) (�C)': 'temperatura_min', 'UMIDADE RELATIVA DO AR, HORARIA (%)': 'humedad_horaria',
            'PRESSAO ATMOSFERICA AO NIVEL DA ESTACAO, HORARIA (mB)': 'pres_atmosferica', 'PRECIPITA��O TOTAL, HOR�RIO (mm)': 'precipitacion_hora',
            'VENTO, RAJADA MAXIMA (m/s)': 'rafaga_max', 'PRESS�O ATMOSFERICA MAX.NA HORA ANT. (AUT) (mB)': 'pres_atmosferica_max', 'PRESS�O ATMOSFERICA MIN. NA HORA ANT. (AUT) (mB)': 'pres_atmosferica_min'}
wind = wind.rename(columns=columnas)
wind.head()
hora direccion_viento velocidad_viento humedad_max humedad_min temperatura_max temperatura_min humedad_horaria pres_atmosferica precipitacion_hora rafaga_max pres_atmosferica_max pres_atmosferica_min
0 12:00 0.809017 1.8 69.0 60.0 22.6 20.7 61.0 888.2 0.0 3.8 888.2 887.7
1 13:00 0.965926 2.7 62.0 55.0 24.2 22.5 55.0 888.4 0.0 4.7 888.4 888.2
2 14:00 0.891007 2.0 56.0 50.0 25.5 24.3 51.0 888.1 0.0 4.9 888.4 888.1
3 15:00 0.848048 2.5 52.0 44.0 27.4 25.0 44.0 887.4 0.0 5.8 888.1 887.4
4 16:00 0.224951 2.4 50.0 43.0 27.1 25.5 46.0 886.5 0.0 5.8 887.4 886.5

Diccionario de variables modificadas:

  • hora: hora

  • direccion_viento: Dirección del viento horaria

  • velocidad_viento: Velocidad horario del viento (m/s) — (Variable predictora)

  • humedad_max: Humedad rel. máx. hora anterior (AUT) ( %)

  • humedad_min: Humedad rel. mín. hora anterior (AUT) ( %)

  • temperatura_max: Temperatura máx. hora anterior (AUT)

  • temperatura_min: Temperatura mín. hora anterior (AUT)

  • humedad_horaria: Humedad relativa horaria ( %)

  • pres_atmosferica: Presión atmosférica a nivel de estación, horaria (mB)

  • precipitacion_hora: Precipitación total por hora (mm)

  • rafaga_max: Máxima ráfaga de viento (m/s)

  • pres_atmosferica_max: Presión atmosférica máx. hora anterior (AUT) (mB)

  • pres_atmosferica_min: Presión atmosférica mín. hora anterior (AUT) (mB)

Ahora analicemos el resumen descriptivo de los datos usando la funcion describe()

wind.describe()
direccion_viento velocidad_viento humedad_max humedad_min temperatura_max temperatura_min humedad_horaria pres_atmosferica precipitacion_hora rafaga_max pres_atmosferica_max pres_atmosferica_min
count 87693.000000 87693.000000 87693.000000 87693.000000 87693.000000 87693.000000 87693.000000 87693.000000 87693.000000 87693.000000 87693.000000 87693.000000
mean 0.405810 2.466192 69.058465 63.176194 21.921264 20.684570 66.146682 887.251925 0.160907 5.161076 887.580724 886.891093
std 0.686247 1.313968 19.640222 20.166336 3.721386 3.513744 19.992327 4.012404 1.307515 2.311157 3.646750 3.564539
min -1.000000 0.000000 12.000000 10.000000 9.200000 8.400000 10.000000 863.400000 0.000000 0.000000 865.300000 862.800000
25% -0.156434 1.500000 54.000000 48.000000 19.200000 18.400000 51.000000 885.300000 0.000000 3.400000 885.600000 885.000000
50% 0.788011 2.400000 72.000000 64.000000 21.400000 20.200000 68.000000 887.200000 0.000000 5.000000 887.500000 886.900000
75% 0.970296 3.400000 87.000000 80.000000 24.700000 23.100000 84.000000 889.100000 0.000000 6.800000 889.300000 888.800000
max 1.000000 10.000000 100.000000 98.000000 35.300000 34.400000 99.000000 1023.500000 70.800000 24.300000 913.100000 910.900000

En primer lugar se observa que para todas las variables se registran 87693 observaciones.

  • velocidad_viento: Con respecto a la variable respuesta, se puede notar que esta variable se distribuye con un mínimo de 0.0 m/s hasta un máximo de 10 m/s, con una velocidad media de 2.46 m/s y una desviación de 1.31. Se puede identificar una dispersión de los datos baja. Asimismo el 50% tiene una velocidad de 2.4 m/s o menos.

  • direccion_viento: para esta variable se observa que se distribuye con un mínimo de -1 hasta un máximo de 1, con una dirección media de 0.4 y una desviación de 0.68. Se puede identificar una dispersión de los datos significativa. Asimismo el 50% de las observaciones tiene una dirección de 0.78 o menos.

  • humedad_max: esta variable se distribuye con una humedad mínima de 12 hasta un máximo de 100, con una humedad media de 69.05 y una desviación de 19.64. Se puede identificar una dispersión de los datos media. Asimismo el 50% de las observaciones tiene una humedad de 72 o menos.

  • humedad_min: esta variable se distribuye con una humedad mínima de 10 hasta un máximo de 98, con una humedad media de 63.176 y una desviación de 20.16. Se puede identificar una dispersión de los datos alta. Asimismo el 25% de las observaciones tiene una humedad de 48 o menos.

  • temperatura_max: esta variable se distribuye con una temperatura mínima de 9.2 hasta un máximo de 35.3, con una temperatura media de 21.92 y una desviación de 3.72. Asimismo el 25% de las observaciones tiene una temperatura de 19.2 o menos, y el 50% de los datos posee una temperatura de 21.4 o menos.

  • temperatura_min: esta variable se distribuye con una temperatura mínima de 3.51 hasta un máximo de 34.4, con una temperatura media de 20.68 y una desviación de 3.51. Asimismo el 25% de las observaciones tiene una temperatura de 18.4o menos, y el 50% de los datos posee una temperatura de 20.2 o menos.

  • humedad_horaria: esta variable se distribuye con una humedad relativa mínima de 10 hasta un máximo de 99, con una humedada media de 66.14 y una desviación de 19.9. Se observa una dispersión alta de los datos.Asimismo el 25% de las observaciones tiene una humedad de 51 o menos, y el 50% de los datos posee una temperatura de 68 o menos.

  • pres_atmosferica: esta variable se distribuye con una presión atmosférica mínima de 863.4 hasta un máximo de 1023.5, con una presión atmosférica media de 887.25 y una desviación de 4.012. Se observa una dispersión baja de los datos. Asimismo el 25% de las observaciones tiene una humedad de 885.3 o menos, y el 50% de los datos posee una temperatura de 887.20 o menos.

  • precipitacion_hora: esta variable se distribuye con una precipitación total mínima de 0 hasta un máximo de 70.8, con una precipitación media de 0.16.25 y una desviación de 1.30.

  • rafaga_max: esta variable se distribuye con una ráfaga de viento mínima de 0 m/s hasta un máximo de 24.3 m/s, con una ráfaga media de 5.16 y una desviación de 2.31. Asimismo el 25% de las observaciones tiene una ráfaga máxima de 3.4 m/s o menos, y el 50% de los datos posee una ráfaga máxima de 6.8 o menos.

  • pres_atmosferica_max: esta variable se distribuye con una presión atmosférica mínima de 865.3 hasta un máximo de 913.1, con una presión atmosférica media de 887.5 y una desviación de 3.64. Se observa una dispersión baja de los datos. Asimismo el 25% de las observaciones tiene una humedad de 865.3 o menos, y el 50% de los datos posee una temperatura de 887.5 o menos.

  • pres_atmosferica_min: esta variable se distribuye con una presión atmosférica mínima de 862.8 hasta un máximo de 910.9, con una presión atmosférica media de 886.89 y una desviación de 3.56 .Se observa una dispersión baja de los datos. Asimismo el 25% de las observaciones tiene una humedad de 885.0 o menos, y el 50% de los datos posee una temperatura de 886.9 o menos.

Ahora veamos el tipo de dato de las variables

wind.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 87693 entries, 0 to 87692
Data columns (total 13 columns):
 #   Column                Non-Null Count  Dtype  
---  ------                --------------  -----  
 0   hora                  87693 non-null  object 
 1   direccion_viento      87693 non-null  float64
 2   velocidad_viento      87693 non-null  float64
 3   humedad_max           87693 non-null  float64
 4   humedad_min           87693 non-null  float64
 5   temperatura_max       87693 non-null  float64
 6   temperatura_min       87693 non-null  float64
 7   humedad_horaria       87693 non-null  float64
 8   pres_atmosferica      87693 non-null  float64
 9   precipitacion_hora    87693 non-null  float64
 10  rafaga_max            87693 non-null  float64
 11  pres_atmosferica_max  87693 non-null  float64
 12  pres_atmosferica_min  87693 non-null  float64
dtypes: float64(12), object(1)
memory usage: 8.7+ MB

Se observa que todas las variables son númericas decimales a excepción de la hora es un string. También, se observa que no hay nulos. Sin embargo, comprobemoslo con la función isna().

datos_faltantes = wind.isna().sum()
porcentaje_faltante = (datos_faltantes / len(wind)) * 100
print(porcentaje_faltante)
print("No hay datos faltantes")
hora                    0.0
direccion_viento        0.0
velocidad_viento        0.0
humedad_max             0.0
humedad_min             0.0
temperatura_max         0.0
temperatura_min         0.0
humedad_horaria         0.0
pres_atmosferica        0.0
precipitacion_hora      0.0
rafaga_max              0.0
pres_atmosferica_max    0.0
pres_atmosferica_min    0.0
dtype: float64
No hay datos faltantes

En efecto, no hay datos faltantes por lo que no es necesario usar técnicas de imputación de los datos.

Histogramas de variables explicativas e independiente

wind.hist(bins=30, figsize=(20, 15), layout=(4, 4), alpha=0.5)
plt.show()
_images/64ea8bb394030e7b5c261663b6a0dc8b7a29318bfd30fd2fa384cb3240eaadc3.png

direccion_viento: La mayoría de las mediciones están alrededor del valor entre 0.5 y 1, lo que indica que hay una dirección predominante del viento.

velocidad_viento: Este histograma muestra que las velocidades más frecuentes del viento están entre 0 y 2, con una disminución en la frecuencia a medida que aumenta la velocidad.

humedad_max: La humedad máxima tiende a distribuirse de manera relativamente uniforme entre aproximadamente el 40% y el 100%, con una ligera concentración de datos hacia el extremo superior.

humedad_min: Los datos de humedad mínima tienden a estar en el rango superior, entre el 60% y el 100%, con muy pocos eventos de baja humedad.

temperatura_max: Las temperaturas máximas se concentran principalmente entre los 20 y los 25 grados, con la frecuencia disminuyendo para temperaturas más altas y más bajas.

temperatura_min: Similar a la temperatura máxima, la mayoría de las temperaturas mínimas se distribuyen entre 15 y 25 grados, con una caída en la frecuencia a valores más bajos y más altos.

humedad_horaria: Este histograma es similar al de la humedad máxima y mínima, indicando que la humedad en una base horaria tiende a estar en el rango medio a alto.

pres_atmosferica: Existe una marcada concentración de valores alrededor de los 900, lo que indica que la presión atmosférica no varía tanto y tiende a ser alta.

precipitacion_hora: La mayoría de las mediciones de precipitación horaria son cercanas a cero, lo que sugiere que la lluvia no es un evento frecuente.

rafaga_max: La velocidad de ráfaga máxima tiende a estar por debajo de los 10, con una frecuencia que disminuye rápidamente a medida que aumenta la velocidad de la ráfaga.

pres_atmosferica_max: La presión atmosférica máxima está muy concentrada alrededor de los valores altos, similar a la presión atmosférica general.

pres_atmosferica_min: Los datos de la presión atmosférica mínima también muestran una fuerte concentración alrededor de los 1000, aunque hay una pequeña cantidad de casos con valores significativamente más bajos.

Análisis de simetría y datos atípicos con boxplot

num_cols = wind.select_dtypes(include=[np.number]).columns

n_cols = 3
n_rows = int(np.ceil(len(num_cols) / n_cols)) 

fig, axes = plt.subplots(n_rows, n_cols, figsize=(15, n_rows*5)) 
fig.suptitle('Boxplots de Variables Numéricas', fontsize=16)

axes = axes.flatten()

for i, col in enumerate(num_cols):
    axes[i].boxplot(wind[col].dropna())
    axes[i].set_title(col)
    axes[i].set_ylabel('Valor')

for i in range(len(num_cols), len(axes)):
    fig.delaxes(axes[i])

plt.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.show()
_images/1077546209fea019c26656d7593eb13345213514e7fc23cfd624e6731e256796.png

direccion_viento: Este diagrama indica que la mediana de la dirección del viento está cerca del cero. El rango intercuartílico (la caja) es muy pequeño, indicando poca variabilidad en la mayoría de los datos, con algunos valores atípicos.

velocidad_viento: La velocidad del viento tiene una mediana relativamente baja y un rango intercuartílico mayor que el de la dirección del viento. Hay varios valores atípicos, que representan velocidades del viento muy altas.

humedad_max: La humedad máxima tiene una mediana por encima del 60% y un rango intercuartílico que sugiere una variabilidad moderada, sin valores atípicos visibles.

humedad_min: La humedad mínima tiene un rango muy amplio y una mediana cercana al 60%. El rango intercuartílico es amplio, mostrando variabilidad, y hay algunos valores atípicos bajos.

temperatura_max: La temperatura máxima muestra una mediana en torno a los 25 grados Celsius, con un rango intercuartílico que indica variabilidad y algunos valores extremos altos.

temperatura_min: Similar a la temperatura máxima, la mínima muestra una mediana alrededor de los 15 grados Celsius, con un rango intercuartílico y valores extremos bajos.

humedad_horaria: Esta variable muestra una distribución similar a la humedad máxima y mínima, con una mediana por encima del 60% y un rango intercuartílico amplio.

pres_atmosferica: La presión atmosférica tiene una mediana muy alta, cerca de los 1000, con un rango intercuartílico estrecho, indicando poca variabilidad, y algunos valores atípicos bajos.

precipitacion_hora: La precipitación horaria tiene muchos valores en cero o cercanos a cero, con algunos valores atípicos que representan episodios de lluvia intensa.

rafaga_max: La velocidad de ráfaga máxima tiene una mediana baja con varios valores atípicos que muestran episodios de ráfagas muy fuertes.

pres_atmosferica_max: La presión atmosférica máxima es consistente con la presión atmosférica general, mostrando una mediana alta y un rango intercuartílico estrecho.

pres_atmosferica_min: Similar a la presión atmosférica máxima, la mínima muestra poca variabilidad y una concentración alta de los datos, con algunos valores extremos bajos.

Scatterplot para cada par de variables

x = wind.columns.drop('velocidad_viento')
y = 'velocidad_viento' 

Ahora, analicemos la correlación entre las variables. Para eso observemos la siguiente matriz de correlación:

wind_num = wind.drop(columns=['hora'])
correlation_matrix = wind_num.corr()


plt.figure(figsize=(10, 8))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', fmt=".2f", linewidths=.5)
plt.title("Correlation Matrix")
plt.show()
_images/a9901ce96a1da17e299e231e39f3654e4bb9a6583a335362518604b6967b5841.png

Se observa una alta correlación entre variables con características similares como humedad_maxima, humedad_horaria y humedad_minima. Esto sucede de forma similar con las variables relacionadas con la presión atmosférica y temperatura. Esto es esperable ya que son variables que se comportan de forma similar y pueden estar explicando lo mismo. Por otro lado, también se observa una baja-media correlación entre variables con características ambientales distintas como la presión atmosférica y la humedad o la precipitación y la humedad.

Realicemos la prueba Variance Inflation Factor (VIF) para reducir la dimensionalidad de nuestro conjunto de datos y eliminar las columnas altamente correlacionadas.

def VIF_calculation(X):
    VIF = pd.DataFrame()
    VIF["variable"] = X.columns
    VIF["VIF"] = [variance_inflation_factor(X.values, i) for i in range(X.shape[1])]
    VIF = VIF.sort_values('VIF', ascending=False).reset_index(drop = True)
    return(VIF)

def delete_multicollinearity(df, target_name, VIF_threshold):
  X = df.drop(target_name, axis=1)
  VIF_mat = VIF_calculation(X)
  n_VIF = VIF_mat["VIF"][0]
  if (n_VIF <= VIF_threshold):
    print("There is no multicollinearity!")
  else:
    while (n_VIF > VIF_threshold):
      X = X.drop(VIF_mat["variable"][0], axis=1)
      VIF_mat = VIF_calculation(X)
      n_VIF = VIF_mat["VIF"][0]
  display(VIF_mat)
  return X

new_wind = delete_multicollinearity(wind_num, 'velocidad_viento', 10)
variable VIF
0 rafaga_max 3.678934
1 humedad_min 3.184922
2 direccion_viento 1.389529
3 precipitacion_hora 1.039001
datos_viento = wind[['rafaga_max', 'humedad_min', 'direccion_viento', 'precipitacion_hora']]
datos_viento.to_csv('datos_viento.csv', index=False)

Luego de realizar la prueba VIF nos quedan las variables que no muestran signos significativos de multicolinealidad. Veamos como queda la matriz de correlación.

plt.figure(figsize=(10, 8))
sns.heatmap(new_wind.corr(), annot=True, cmap='coolwarm', fmt=".2f", linewidths=.5)
plt.title("Correlation Matrix")
plt.show()
_images/a1bf19b4ef3f95ba2b3d9212ede590e40594596bdf8ac0e05e981fa774534848.png

Obervamos que ahora los coeficientes de correlación de las variables explicativas son bajos, lo que indica que las variables no explican lo mismo entre si.

Ahora, veamos la distribución de nuestras variables explicativas a través de histogramas

new_wind['velocidad_viento'] = wind['velocidad_viento']
new_wind.hist(figsize=(10, 8))
plt.tight_layout()
plt.show()
_images/5e656d77f6fc3fe76c75d394c474f16de0524dbf8ddf52a3a8c5a921a357e54c.png

Observamos que las variables velocidad_viento, rafaga_max y precipitacion_hora presentan una distribución sesgada a la izquierda. Mientras que direccion_viento y humedad_min presentan un sesgo a la derecha. Los datos no están centrados en torno a la media y están dispersos.

Análisis de series de tiempo#

start_date = datetime.date(2010, 1, 1)

def process_hours(hours):
    datetimes = []
    current_date = start_date
    previous_hour = 24 

    for time in hours:
        hour = int(time.split(":")[0])
        if hour < previous_hour:
            if previous_hour != 24: 
                current_date += datetime.timedelta(days=1)
        datetime_obj = datetime.datetime.combine(current_date, datetime.time(hour=hour))
        datetimes.append(datetime_obj)
        previous_hour = hour

    return datetimes

wind['Fecha'] = process_hours(wind['hora'])
wind.head()
hora direccion_viento velocidad_viento humedad_max humedad_min temperatura_max temperatura_min humedad_horaria pres_atmosferica precipitacion_hora rafaga_max pres_atmosferica_max pres_atmosferica_min Fecha
0 12:00 0.809017 1.8 69.0 60.0 22.6 20.7 61.0 888.2 0.0 3.8 888.2 887.7 2010-01-01 12:00:00
1 13:00 0.965926 2.7 62.0 55.0 24.2 22.5 55.0 888.4 0.0 4.7 888.4 888.2 2010-01-01 13:00:00
2 14:00 0.891007 2.0 56.0 50.0 25.5 24.3 51.0 888.1 0.0 4.9 888.4 888.1 2010-01-01 14:00:00
3 15:00 0.848048 2.5 52.0 44.0 27.4 25.0 44.0 887.4 0.0 5.8 888.1 887.4 2010-01-01 15:00:00
4 16:00 0.224951 2.4 50.0 43.0 27.1 25.5 46.0 886.5 0.0 5.8 887.4 886.5 2010-01-01 16:00:00
fig = px.line(wind, x="Fecha", y='velocidad_viento', title='Velocidad del Viento por Hora')
fig
  • Fluctuaciones Constantes: La velocidad del viento muestra fluctuaciones constantes a lo largo del tiempo, sin una tendencia clara de aumento o disminución a largo plazo.

  • Picos Específicos: Se observan varios picos que alcanzan o superan las 10 unidades en la escala vertical. Estos picos podrían representar eventos de vientos fuertes específicos, como tormentas o frentes de ráfaga.

  • Variabilidad: La mayor parte del tiempo, la velocidad del viento parece oscilar entre aproximadamente 4 y 6 unidades, con picos ocasionales que la llevan más allá de estos valores. Esta variabilidad puede ser influenciada por factores estacionales, como mencioné anteriormente, o por cambios en los patrones climáticos locales.

average_wind_speed = wind.groupby('hora')['velocidad_viento'].mean().reset_index()

fig = px.bar(average_wind_speed, x='hora', y='velocidad_viento', title='Promedio de Velocidad del Viento por Hora del Día')
fig.update_layout(xaxis_title='Hora del Día', yaxis_title='Velocidad del Viento Promedio', 
                  xaxis=dict(tickmode='linear'), yaxis=dict(tickmode='linear'))
fig

A partir del gráfico, podemos observar algunas tendencias interesantes en la velocidad del viento a lo largo del día:

  • Períodos de menor velocidad del viento: Durante las primeras horas del día, desde las 00:00 hasta aproximadamente las 06:00, la velocidad del viento es relativamente baja, con valores que parecen estar generalmente por debajo de 2 en la escala del gráfico.

  • Aumento matutino: A partir de las 07:00, hay un notable aumento en la velocidad del viento, alcanzando un pico alrededor de las 10:00. Esto podría sugerir un patrón común donde el viento se intensifica a medida que el sol calienta la superficie terrestre, disminuyendo la estabilidad atmosférica y permitiendo que el viento se intensifique.

  • Máximo en la tarde: La velocidad del viento sigue siendo alta durante las horas de la tarde, con otro pico pronunciado alrededor de las 15:00. Este comportamiento puede ser típico de áreas donde los procesos térmicos diurnos o los patrones locales de circulación como las brisas marinas o montañosas juegan un papel importante.

  • Disminución vespertina y nocturna: Después de las 18:00, la velocidad del viento comienza a disminuir gradualmente, llegando a niveles más bajos similares a los observados en las primeras horas de la mañana.

wind['Fecha'] = pd.to_datetime(wind['Fecha'])
wind.set_index('Fecha', inplace=True)

years = (wind.index.max() - wind.index.min()).days / 365.25

segments = np.array_split(wind, 3) 

for i, segment in enumerate(segments):
    period = int((segment.index.max() - segment.index.min()).days / 3)
    result = seasonal_decompose(segment['velocidad_viento'], model='additive', period=max(period, 1))
    
    plt.figure(figsize=(12, 8))
    result.plot()
    plt.suptitle(f'Decomposition of Segment {i+1}')
    plt.tight_layout()
    plt.show()
/Users/kennyzhu/Documents/parcial3-eda/.venv/lib/python3.11/site-packages/numpy/core/fromnumeric.py:59: FutureWarning:

'DataFrame.swapaxes' is deprecated and will be removed in a future version. Please use 'DataFrame.transpose' instead.
<Figure size 1200x800 with 0 Axes>
_images/3eacfcb88d21c052e90dad388cda57061d4dfa21fc55699d0dc8e2dd252e0eb7.png
<Figure size 1200x800 with 0 Axes>
_images/d1c59a244c8634983efcaeb9c178bb6e91f048cd1ddc04a166501dc33ea4b2b0.png
<Figure size 1200x800 with 0 Axes>
_images/6d77486420ff0066820f6c4a162c7a1985fed1f85f52d905648582d536664094.png
  • Componente Original: La serie de tiempo superior muestra los datos originales de la velocidad del viento, que varían entre 0 y 10. Las variaciones son bastante abruptas y frecuentes, lo que es típico para datos meteorológicos como la velocidad del viento.

  • Tendencia: La segunda gráfica muestra la tendencia de los datos. Aquí observamos que la velocidad del viento tiene fluctuaciones significativas a lo largo del tiempo, con picos y valles que sugieren variaciones estacionales o influencias de patrones climáticos más amplios. La tendencia parece variar principalmente entre 2 y 3.5, indicando que, a pesar de la volatilidad a corto plazo, la velocidad media del viento se mantiene en un rango relativamente estrecho.

  • Estacionalidad: tiene una periodicidad clara y consistente. Este patrón repetido sugiere que la velocidad del viento es afectada por factores que tienen un ciclo anual regular, lo que podría estar relacionado con cambios estacionales en el clima. La amplitud de la estacionalidad es pequeña (aproximadamente 0.5), lo que indica que, aunque consistente, la influencia estacional no domina completamente la variabilidad de la velocidad del viento.

  • Residuos: La gráfica de los residuos, es decir, lo que queda después de haber removido los efectos de la tendencia y la estacionalidad. Los residuos parecen ser bastante bajos y consistentemente cercanos a cero, con algunos valores atípicos. Esto es una buena señal de que el modelo de descomposición ha capturado la mayoría de las estructuras de los datos, y los residuos no muestran patrones aparentes que el modelo haya pasado por alto.

Fraud Detection EDA#

Iniciamos cargando los conjunto de datos y observando las primeras observaciones

identity = pd.read_csv('data/train_identity.csv')
identity.head()
TransactionID id_01 id_02 id_03 id_04 id_05 id_06 id_07 id_08 id_09 ... id_31 id_32 id_33 id_34 id_35 id_36 id_37 id_38 DeviceType DeviceInfo
0 2987004 0.0 70787.0 NaN NaN NaN NaN NaN NaN NaN ... samsung browser 6.2 32.0 2220x1080 match_status:2 T F T T mobile SAMSUNG SM-G892A Build/NRD90M
1 2987008 -5.0 98945.0 NaN NaN 0.0 -5.0 NaN NaN NaN ... mobile safari 11.0 32.0 1334x750 match_status:1 T F F T mobile iOS Device
2 2987010 -5.0 191631.0 0.0 0.0 0.0 0.0 NaN NaN 0.0 ... chrome 62.0 NaN NaN NaN F F T T desktop Windows
3 2987011 -5.0 221832.0 NaN NaN 0.0 -6.0 NaN NaN NaN ... chrome 62.0 NaN NaN NaN F F T T desktop NaN
4 2987016 0.0 7460.0 0.0 0.0 1.0 0.0 NaN NaN 0.0 ... chrome 62.0 24.0 1280x800 match_status:2 T F T T desktop MacOS

5 rows × 41 columns

identity.shape
(144233, 41)

El dataset cuenta con 41 columnas por 144233 observaciones

transaction = pd.read_csv('data/train_transaction.csv')
transaction.head()
TransactionID isFraud TransactionDT TransactionAmt ProductCD card1 card2 card3 card4 card5 ... V330 V331 V332 V333 V334 V335 V336 V337 V338 V339
0 2987000 0 86400 68.5 W 13926 NaN 150.0 discover 142.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1 2987001 0 86401 29.0 W 2755 404.0 150.0 mastercard 102.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2 2987002 0 86469 59.0 W 4663 490.0 150.0 visa 166.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
3 2987003 0 86499 50.0 W 18132 567.0 150.0 mastercard 117.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
4 2987004 0 86506 50.0 H 4497 514.0 150.0 mastercard 102.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0

5 rows × 394 columns

transaction.shape
(590540, 394)

Se observa que el dataset cuenta con 590540 observaciones con 394 columnas

Ahora analicemos la variable respuesta isFraud

fraude = {0: 'No es fraude', 1: 'Es fraude'}


transaction['mapeo'] = transaction['isFraud'].map(fraude)

plt.figure(figsize=(8, 6))
sns.countplot(x=transaction['mapeo'])
plt.xlabel('Fraude')
plt.ylabel('Número de transacciones')
plt.title('Distribución de la variable isFraud')
plt.show()
_images/5b2c1be5114c2bc026c4c3a70746215fbe7d3041ec99b92fcf134a58bf99364d.png

Se observa que la mayoría de datos en el datset corresponde a la categoría ‘No es fraude’, mientras que hay muchos menos datos de la categoría ‘Es fraude’. Esto significa que tenemos un conjunto de datos desbalanceados.

Procedamos a ver el tipo de datos de las variables:

identity.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 144233 entries, 0 to 144232
Data columns (total 41 columns):
 #   Column         Non-Null Count   Dtype  
---  ------         --------------   -----  
 0   TransactionID  144233 non-null  int64  
 1   id_01          144233 non-null  float64
 2   id_02          140872 non-null  float64
 3   id_03          66324 non-null   float64
 4   id_04          66324 non-null   float64
 5   id_05          136865 non-null  float64
 6   id_06          136865 non-null  float64
 7   id_07          5155 non-null    float64
 8   id_08          5155 non-null    float64
 9   id_09          74926 non-null   float64
 10  id_10          74926 non-null   float64
 11  id_11          140978 non-null  float64
 12  id_12          144233 non-null  object 
 13  id_13          127320 non-null  float64
 14  id_14          80044 non-null   float64
 15  id_15          140985 non-null  object 
 16  id_16          129340 non-null  object 
 17  id_17          139369 non-null  float64
 18  id_18          45113 non-null   float64
 19  id_19          139318 non-null  float64
 20  id_20          139261 non-null  float64
 21  id_21          5159 non-null    float64
 22  id_22          5169 non-null    float64
 23  id_23          5169 non-null    object 
 24  id_24          4747 non-null    float64
 25  id_25          5132 non-null    float64
 26  id_26          5163 non-null    float64
 27  id_27          5169 non-null    object 
 28  id_28          140978 non-null  object 
 29  id_29          140978 non-null  object 
 30  id_30          77565 non-null   object 
 31  id_31          140282 non-null  object 
 32  id_32          77586 non-null   float64
 33  id_33          73289 non-null   object 
 34  id_34          77805 non-null   object 
 35  id_35          140985 non-null  object 
 36  id_36          140985 non-null  object 
 37  id_37          140985 non-null  object 
 38  id_38          140985 non-null  object 
 39  DeviceType     140810 non-null  object 
 40  DeviceInfo     118666 non-null  object 
dtypes: float64(23), int64(1), object(17)
memory usage: 45.1+ MB
transaction.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 590540 entries, 0 to 590539
Columns: 395 entries, TransactionID to mapeo
dtypes: float64(376), int64(4), object(15)
memory usage: 1.7+ GB

Este dataset posee variables tanto numéricas como categóricas. Asimismo, se pueden observar datos faltantes. Más adelante revisaremos este aspecto a detalle.

A continuación veamos los datos faltantes.

proporcion_datos_vacios_identity = identity.isna().mean()
proporcion_datos_vacios_transaction= transaction.isna().mean()
data = {
    'Dataset': ['Indentity', 'Transaction'],
    'Proporción de datos vacíos': [proporcion_datos_vacios_identity.mean(), proporcion_datos_vacios_transaction.mean()]
}

df_proporcion_vacios = pd.DataFrame(data)

plt.figure(figsize=(8, 6))
ax = sns.barplot(x='Dataset', y='Proporción de datos vacíos', data=df_proporcion_vacios, color='skyblue')
plt.title('Proporción de datos vacíos en los conjuntos de datos')
plt.ylim(0, 1)  

for p in ax.patches:
    ax.annotate(f'{p.get_height() * 100:.2f}%', (p.get_x() + p.get_width() / 2., p.get_height()),
                ha='center', va='center', fontsize=12, color='black', xytext=(0, 10),
                textcoords='offset points')

plt.show()
_images/702181eddcd336313af37528c131ee5a4881a30058196c71b733e7fbd4ae5da8.png

Veamos los datos faltantes por columna de cada dataset

datos_faltantes = identity.isna().sum()
porcentaje_faltante = (datos_faltantes / len(identity)) * 100
print(porcentaje_faltante)
TransactionID     0.000000
id_01             0.000000
id_02             2.330257
id_03            54.016071
id_04            54.016071
id_05             5.108401
id_06             5.108401
id_07            96.425922
id_08            96.425922
id_09            48.052110
id_10            48.052110
id_11             2.256765
id_12             0.000000
id_13            11.726165
id_14            44.503685
id_15             2.251912
id_16            10.325654
id_17             3.372321
id_18            68.722137
id_19             3.407681
id_20             3.447200
id_21            96.423149
id_22            96.416215
id_23            96.416215
id_24            96.708798
id_25            96.441868
id_26            96.420375
id_27            96.416215
id_28             2.256765
id_29             2.256765
id_30            46.222432
id_31             2.739318
id_32            46.207872
id_33            49.187079
id_34            46.056034
id_35             2.251912
id_36             2.251912
id_37             2.251912
id_38             2.251912
DeviceType        2.373243
DeviceInfo       17.726179
dtype: float64
datos_faltantes = transaction.isna().sum()
porcentaje_faltante = (datos_faltantes / len(transaction)) * 100
print(porcentaje_faltante)
TransactionID      0.000000
isFraud            0.000000
TransactionDT      0.000000
TransactionAmt     0.000000
ProductCD          0.000000
                    ...    
V336              86.054967
V337              86.054967
V338              86.054967
V339              86.054967
mapeo              0.000000
Length: 395, dtype: float64

En ambos dataset la mayoría de columnas tienen un porcentaje significativo de datos faltantes.

Veamos un análisis general de las variables por grupo.

fig, axes = plt.subplots(1, 3, figsize=(18, 6))

# Histograma con todos los datos
sns.histplot(transaction["TransactionDT"], bins=500, kde=True, color='skyblue', ax=axes[0])
axes[0].set_title('Todos los datos')
axes[0].set_xlabel('TransactionDT')
axes[0].set_ylabel('Frecuencia')

# Histograma donde la columna isFraud = 0
sns.histplot(transaction[transaction['mapeo'] == 'No es fraude']["TransactionDT"], bins=500, kde=True, color='green', ax=axes[1])
axes[1].set_title('isFraud = 0')
axes[1].set_xlabel('TransactionDT')
axes[1].set_ylabel('Frecuencia')

# Histograma donde la columna isFraud = 1
sns.histplot(transaction[transaction['mapeo'] == 'Es fraude']["TransactionDT"], bins=500, kde=True, color='red', ax=axes[2])
axes[2].set_title('isFraud = 1')
axes[2].set_xlabel('TransactionDT')
axes[2].set_ylabel('Frecuencia')

plt.tight_layout()  
plt.show()
_images/b6b08d1cb3e9fd7e097053312a5d7427d2526e2dce5da38387951e842ec9d8b9.png
  1. Todos los datos: Hay picos claros y valles que sugieren patrones temporales o cíclicos en los datos. Estos podrían corresponder a horarios de alta actividad como las horas del día o días específicos de la semana.

  2. isFraud = 0: Este sigue un patrón similar al del gráfico de todos los datos, lo que tiene sentido ya que las transacciones legítimas constituyen la mayoría de los datos y, por lo tanto, dominan el patrón general observado.

  3. isFraud = 1: La escala del eje y es mucho menor en comparación con los otros dos gráficos, lo que refleja la menor frecuencia de casos de fraude en el conjunto de datos. Los picos aquí parecen menos pronunciados y el patrón temporal puede diferir del de las transacciones legítimas.

card= [f"card{i}" for i in range(1,7)]
cards=transaction[card]

fig, axes = plt.subplots(nrows=1, ncols=3, figsize=(24, 6))

for i in range(0, 3):
    sns.distplot(cards.iloc[:, i], bins=50, ax=axes[i], hist_kws={'edgecolor': 'black', 'linewidth': 1.2}, kde= True)

    axes[i].set_title('Distribution of ' + cards.iloc[:, i].name, fontsize=16)
    axes[i].set_xlabel(cards.iloc[:, i].name, fontsize=14)
    axes[i].set_ylabel('Density', fontsize=14)

    axes[i].grid(True, linestyle='--', alpha=0.6)

    axes[i].set_facecolor('lightgray')

plt.tight_layout()
plt.show()
/var/folders/4x/by41vdp921s318y3mybq7fb80000gn/T/ipykernel_9917/2327863681.py:7: UserWarning:



`distplot` is a deprecated function and will be removed in seaborn v0.14.0.

Please adapt your code to use either `displot` (a figure-level function with
similar flexibility) or `histplot` (an axes-level function for histograms).

For a guide to updating your code to use the new functions, please see
https://gist.github.com/mwaskom/de44147ed2974457ad6372750bbe5751
/var/folders/4x/by41vdp921s318y3mybq7fb80000gn/T/ipykernel_9917/2327863681.py:7: UserWarning:



`distplot` is a deprecated function and will be removed in seaborn v0.14.0.

Please adapt your code to use either `displot` (a figure-level function with
similar flexibility) or `histplot` (an axes-level function for histograms).

For a guide to updating your code to use the new functions, please see
https://gist.github.com/mwaskom/de44147ed2974457ad6372750bbe5751
/var/folders/4x/by41vdp921s318y3mybq7fb80000gn/T/ipykernel_9917/2327863681.py:7: UserWarning:



`distplot` is a deprecated function and will be removed in seaborn v0.14.0.

Please adapt your code to use either `displot` (a figure-level function with
similar flexibility) or `histplot` (an axes-level function for histograms).

For a guide to updating your code to use the new functions, please see
https://gist.github.com/mwaskom/de44147ed2974457ad6372750bbe5751
_images/def96582008333722b12490211a523593aec4affbd4a1513e104a3f200744225.png

Distribución de card1: Este histograma muestra una distribución bastante uniforme con múltiples picos, lo que indica que los valores para la card1 están distribuidos a lo largo de un amplio rango de cantidades. Hay varios picos notables que sugieren que ciertas cantidades ocurren con más frecuencia que otras.

Distribución de card2: El histograma para la card2 revela una distribución más irregular con varios picos agudos, lo que podría sugerir que hay cantidades de transacciones que son muy populares y posiblemente algunas cantidades que son menos comunes.

Distribución de card3: El histograma para la card3 es bastante diferente de los dos primeros. Muestra un pico muy fuerte en un valor particular, lo que podría implicar que la mayoría de las transacciones para la card3 son de una cantidad única, con muy pocas transacciones de otras cantidades. Hay un pico más pequeño en un valor más alto también, indicando una cantidad común secundaria, pero es mucho menos frecuente que la principal.

fig, ax = plt.subplots(1, 2, figsize=(25,5))

sns.countplot(x="card4", ax=ax[0], data=transaction.loc[transaction['mapeo'] == "No es fraude"])
ax[0].set_title('card4 isFraud=0', fontsize=14)
sns.countplot(x="card4", ax=ax[1], data=transaction.loc[transaction['mapeo'] == "Es fraude"])
ax[1].set_title('card4 isFraud=1', fontsize=14)
plt.show()
_images/0da1229a893adf41ad06534b35595a85ff4e26470ef537acbddae226cbc66c4a.png

card4 isFraud=0: Este diagrama muestra que las transacciones no fraudulentas están dominadas por Visa, seguida por Mastercard, y luego Discover, con American Express siendo la menos utilizada entre las presentadas. La barra de Visa es significativamente más alta que las demás, lo que indica una gran cantidad de transacciones legítimas con tarjetas Visa.

card4 isFraud=1: Visa y Mastercard son considerablemente más altas que Discover y American Express, lo cual indica que la mayoría de las transacciones fraudulentas se realizaron con Visa y Mastercard.

fig, ax = plt.subplots(1, 2, figsize=(25,5))

sns.countplot(x="card6", ax=ax[0], data=transaction.loc[transaction['mapeo'] == "No es fraude"])
ax[0].set_title('card6 isFraud=0', fontsize=14)
sns.countplot(x="card6", ax=ax[1], data=transaction.loc[transaction['mapeo'] == "Es fraude"])
ax[1].set_title('card6 isFraud=1', fontsize=14)
plt.show()
_images/cb2b7684e563c3357a4edfe6a5d62330d5744648c3f4096ce3d721a0a78d4107.png

card6 isFraud=0: En el primer gráfico, se muestra que la mayoría de las transacciones no fraudulentas se realizan con tarjetas de débito, seguidas por las de crédito. La categoría “debit or credit” y “charge card” tienen una representación mucho menor. Esto sugiere que las tarjetas de débito son mucho más utilizadas para transacciones legítimas en este conjunto de datos.

card6 isFraud=1: El segundo gráfico, que representa las transacciones marcadas como fraudulentas, muestra un patrón similar con la mayoría de las transacciones fraudulentas realizadas con tarjetas de débito. Las tarjetas de crédito también tienen una participación significativa en el fraude, aunque ligeramente menor que las de débito.

Selección de Variables (Transaction)#

Iniciareemos eliminando aquellas columnas con más de 50% de datos nulos.

def eliminar_columnas_nulos(df, porcentaje_umbral=0.5):
    num_min_no_nulos = len(df) * porcentaje_umbral
    df_filtrado = df.dropna(axis=1, thresh=num_min_no_nulos)
    return df_filtrado

identity = eliminar_columnas_nulos(identity, 0.5)
transaction = eliminar_columnas_nulos(transaction, 0.5)

print("Variables Identity",identity.columns)
print("Variables Transaction", transaction.columns)
transaction.shape[1]
Variables Identity Index(['TransactionID', 'id_01', 'id_02', 'id_05', 'id_06', 'id_09', 'id_10',
       'id_11', 'id_12', 'id_13', 'id_14', 'id_15', 'id_16', 'id_17', 'id_19',
       'id_20', 'id_28', 'id_29', 'id_30', 'id_31', 'id_32', 'id_33', 'id_34',
       'id_35', 'id_36', 'id_37', 'id_38', 'DeviceType', 'DeviceInfo'],
      dtype='object')
Variables Transaction Index(['TransactionID', 'isFraud', 'TransactionDT', 'TransactionAmt',
       'ProductCD', 'card1', 'card2', 'card3', 'card4', 'card5',
       ...
       'V313', 'V314', 'V315', 'V316', 'V317', 'V318', 'V319', 'V320', 'V321',
       'mapeo'],
      dtype='object', length=221)
221

Estas son las columnas que quedaron luego de eliminar las variables con más del 50% de datos faltantes.

Variables C y D#

numeric_vars_with_d = [col for col in transaction.select_dtypes(include=['number']).columns if col.startswith('D')]
if numeric_vars_with_d:
    print("Variables numéricas que empiezan por 'D':", numeric_vars_with_d)
    transaction_d = transaction[numeric_vars_with_d]
    correlacion = transaction_d.corr()
    plt.figure(figsize=(10, 8))
    sns.heatmap(correlacion, annot=True, cmap='coolwarm', fmt=".2f", linewidths=.5)
    plt.title('Matriz de correlación de variables numéricas que empiezan por D')
    plt.show()
else:
    print("No hay variables numéricas que empiecen por 'D'.")

categorical_vars_with_d = [col for col in transaction.select_dtypes(include=['object']).columns if col.startswith('D')]
if categorical_vars_with_d:
    print("Variables categóricas que empiezan por 'D':", categorical_vars_with_d)
else:
    print("No hay variables categóricas que empiecen por 'D'.")
Variables numéricas que empiezan por 'D': ['D1', 'D2', 'D3', 'D4', 'D10', 'D11', 'D15']
_images/46a65cf6682f6ea26496f0cfc8fb1b42ad4b648e2db338e40000c619ccf6f3a2.png
No hay variables categóricas que empiecen por 'D'.

Seleccionamos las columnas que tengan la menor correlación entre ellas para evitar la colinealidad. Estas son: D3, D4, D11 y eliminamos el resto de variables que empiecen por D.

columnas_a_mantener = ['D3', 'D4', 'D11']
columnas_a_eliminar = [columna for columna in transaction.columns if columna.startswith('D') and columna not in columnas_a_mantener]

transaction = transaction.drop(columnas_a_eliminar, axis=1)

Ahora hagamos el mismo procedimiento con las variables que empiecen por C.

numeric_vars_with_d = [col for col in transaction.select_dtypes(include=['number']).columns if col.startswith('C')]
if numeric_vars_with_d:
    print("Variables numéricas que empiezan por 'C':", numeric_vars_with_d)
    transaction_d = transaction[numeric_vars_with_d]
    correlacion = transaction_d.corr()
    plt.figure(figsize=(10, 8))
    sns.heatmap(correlacion, annot=True, cmap='coolwarm', fmt=".2f", linewidths=.5)
    plt.title('Matriz de correlación de variables numéricas que empiezan por C')
    plt.show()
else:
    print("No hay variables numéricas que empiecen por 'C'.")

categorical_vars_with_d = [col for col in transaction.select_dtypes(include=['object']).columns if col.startswith('C')]
if categorical_vars_with_d:
    print("Variables categóricas que empiezan por 'C':", categorical_vars_with_d)
else:
    print("No hay variables categóricas que empiecen por 'C'.")
Variables numéricas que empiezan por 'C': ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9', 'C10', 'C11', 'C12', 'C13', 'C14']
_images/c91d140ff515a2ab9690cb3a93cdb02b85f6dd1338cb05e5e9669a49d35666b7.png
No hay variables categóricas que empiecen por 'C'.

Al igual que en el caso anterior se seleccionan las variables que tengan la menos correlación entre ellas. Seleccionamos las columnas C3, C5 y eliminamos las otras del dataset.

columnas_a_mantener = ['C3', 'C5']
columnas_a_eliminar = [columna for columna in transaction.columns if columna.startswith('C') and columna not in columnas_a_mantener]

transaction = transaction.drop(columnas_a_eliminar, axis=1)

Variables Card#

Ahora revisemos los coeficientes de correlación de las variables numéricas que inicien con ‘card’.

numeric_vars_with_card = [col for col in transaction.select_dtypes(include=['number']).columns if col.startswith('card')]
if numeric_vars_with_card:
    print("Variables numéricas que empiezan por 'card':", numeric_vars_with_card)
    transaction_card = transaction[numeric_vars_with_card]
    correlacion = transaction_card.corr()

    plt.figure(figsize=(10, 8))
    sns.heatmap(correlacion, annot=True, cmap='coolwarm', fmt=".2f", linewidths=.5)
    plt.title('Matriz de correlación de variables numéricas que empiezan por card')
    plt.show()
else:
    print("No hay variables numéricas que empiecen por 'card'.")

categorical_vars_with_card = [col for col in transaction.select_dtypes(include=['object']).columns if col.startswith('card')]
if categorical_vars_with_card:
    print("Variables categóricas que empiezan por 'card':", categorical_vars_with_card)

    print('Prueba Chi-Cuadrado')
    columns_to_keep = []
    for col1 in categorical_vars_with_card:
        is_independent = True
        for col2 in categorical_vars_with_card:
            if col1 != col2:
                contingency_table = pd.crosstab(transaction[col1], transaction[col2])
                chi2, p, dof, expected = chi2_contingency(contingency_table)
                if p < 0.05:
                    is_independent = False
                    break
        if is_independent:
            columns_to_keep.append(col1)

    df_reduced = transaction[columns_to_keep]
    print("Columnas mantenidas:", columns_to_keep)
    if not columns_to_keep:
        print("No se mantuvo ninguna columna.")
    else:
        print(df_reduced.head())
else:
    print("No hay variables categóricas que empiecen por 'card'.")
Variables numéricas que empiezan por 'card': ['card1', 'card2', 'card3', 'card5']
_images/56e0d2023b0d0923a4a500562610b99a05caa29c54ada8578a32ffdf899a66a0.png
Variables categóricas que empiezan por 'card': ['card4', 'card6']
Prueba Chi-Cuadrado
Columnas mantenidas: []
No se mantuvo ninguna columna.

Con este conjunto de variables podemos observar que en general tienen baja correlación entre ellas, por lo que se escogeremos las siguientes: card2, card3 y eliminamos el resto.

columnas_a_mantener = ['card2', 'card3']
columnas_a_eliminar = [columna for columna in transaction.columns if columna.startswith('card') and columna not in columnas_a_mantener]

transaction = transaction.drop(columnas_a_eliminar, axis=1)

Variables M#

Revisemos las variables numéricas que empiezan por M.

df_numeric = transaction.select_dtypes(include=['number'])
columnas_con_M = [columna for columna in df_numeric.columns if columna.startswith('M')]
print(columnas_con_M)
[]

Como podemos ver no hay variables numéricas que empiezan por M. Así que aplicaremos la prueba Chi-cuadrado a las variables categóricas.

transaction_category = transaction.select_dtypes(include=['object'])
columnas_con_m = [columna for columna in transaction_category.columns if columna.startswith('M')]
transaction_m = transaction_category[columnas_con_m]
print(transaction_m)

columns_to_keep = []

for col1 in transaction_m.columns:
    is_independent = True
    for col2 in transaction_m.columns:
        if col1 != col2:
            contingency_table = pd.crosstab(transaction_m[col1], transaction_m[col2])
            chi2, p, dof, expected = chi2_contingency(contingency_table)
            if p < 0.05:
                is_independent = False
                break
    if is_independent:
        columns_to_keep.append(col1)
df_reduced = transaction_m[columns_to_keep]
print("Columnas mantenidas:", columns_to_keep)
print(df_reduced.head())
         M1   M2   M3   M4   M6
0         T    T    T   M2    T
1       NaN  NaN  NaN   M0    T
2         T    T    T   M0    F
3       NaN  NaN  NaN   M0    F
4       NaN  NaN  NaN  NaN  NaN
...     ...  ...  ...  ...  ...
590535    T    T    T   M0    F
590536    T    F    F   M0    T
590537    T    F    F  NaN    T
590538    T    T    T   M0    T
590539    T    F    F  NaN    T

[590540 rows x 5 columns]
Columnas mantenidas: []
Empty DataFrame
Columns: []
Index: [0, 1, 2, 3, 4]

Ninguna variable pasó la prueba Chi-cuadrado, esto significa que son dependientes.

Variables Product#

Una vez más realizamos el mismo procedimiento pero en este caso analizaremos las variables que inician por ‘Product’

numeric_vars_with_d = [col for col in transaction.select_dtypes(include=['number']).columns if col.startswith('ProductCD')]
if numeric_vars_with_d:
    print("Variables numéricas que empiezan por 'ProductCD_':", numeric_vars_with_d)
    transaction_d = transaction[numeric_vars_with_d]
    correlacion = transaction_d.corr()
    plt.figure(figsize=(10, 8))
    sns.heatmap(correlacion, annot=True, cmap='coolwarm', fmt=".2f", linewidths=.5)
    plt.title('Matriz de correlación de variables numéricas que empiezan por ProductCD_')
    plt.show()
else:
    print("No hay variables numéricas que empiecen por 'ProductCD_'.")

categorical_vars_with_d = [col for col in transaction.select_dtypes(include=['object']).columns if col.startswith('ProductCD_')]
if categorical_vars_with_d:
    print("Variables categóricas que empiezan por 'ProductCD_':", categorical_vars_with_d)
else:
    print("No hay variables categóricas que empiecen por 'ProductCD_'.")
No hay variables numéricas que empiecen por 'ProductCD_'.
No hay variables categóricas que empiecen por 'ProductCD_'.

Luego de hacer la anterior selección de variables el dataset resultante sería:

final_transaction = transaction[['isFraud', 'TransactionID','TransactionDT', 'TransactionAmt',  'card2', 'card3', 
                                 'C3', 'C5', 'D3', 'D4', 'D11']]
final_transaction
isFraud TransactionID TransactionDT TransactionAmt card2 card3 C3 C5 D3 D4 D11
0 0 2987000 86400 68.50 NaN 150.0 0.0 0.0 13.0 NaN 13.0
1 0 2987001 86401 29.00 404.0 150.0 0.0 0.0 NaN 0.0 NaN
2 0 2987002 86469 59.00 490.0 150.0 0.0 0.0 NaN 0.0 315.0
3 0 2987003 86499 50.00 567.0 150.0 0.0 0.0 0.0 94.0 NaN
4 0 2987004 86506 50.00 514.0 150.0 0.0 0.0 NaN NaN NaN
... ... ... ... ... ... ... ... ... ... ... ...
590535 0 3577535 15811047 49.00 NaN 150.0 0.0 1.0 30.0 NaN 56.0
590536 0 3577536 15811049 39.50 225.0 150.0 0.0 0.0 NaN 0.0 0.0
590537 0 3577537 15811079 30.95 595.0 150.0 0.0 1.0 NaN 0.0 0.0
590538 0 3577538 15811088 117.00 481.0 150.0 0.0 0.0 0.0 22.0 22.0
590539 0 3577539 15811131 279.95 170.0 150.0 0.0 1.0 0.0 1.0 0.0

590540 rows × 11 columns

One hot encoder#

#Dataset Identity
data1_string = identity.select_dtypes(include=['object'])
encoder1 = OneHotEncoder()
data1_encoded = encoder1.fit_transform(data1_string)
data1_encoded_df = pd.DataFrame(data1_encoded.toarray(), columns=encoder1.get_feature_names_out(data1_string.columns))
identity = pd.concat([identity.drop(columns=data1_string.columns), data1_encoded_df], axis=1)

#Dataset Transaction
data2_string = final_transaction.select_dtypes(include=['object'])
encoder2 = OneHotEncoder()
data2_encoded = encoder2.fit_transform(data2_string)
data2_encoded_df = pd.DataFrame(data2_encoded.toarray(), columns=encoder2.get_feature_names_out(data2_string.columns))
final_transaction = pd.concat([final_transaction.drop(columns=data2_string.columns), data2_encoded_df], axis=1)

Antes de realizar la prueba VIF realicemos la imputación de datos.

#imputación datos categoricos por dato mas frecuente
identity = identity.apply(lambda x: x.fillna(x.value_counts().index[0]) if x.dtype == 'O' else x)
final_transaction = final_transaction.apply(lambda x: x.fillna(x.value_counts().index[0]) if x.dtype == 'O' else x)

#imputar con la mediana
identity = identity.fillna(identity.median())
final_transaction = final_transaction.fillna(final_transaction.median())

Por último realicemos la prueba VIF para asegurarnos que no haya multicolinelidad entre las variables explicativas.

def VIF_calculation(X):
    """ Calcula el VIF para cada variable en el DataFrame X. """
    VIF_data = pd.DataFrame()
    VIF_data["variable"] = X.columns
    VIF_data["VIF"] = [variance_inflation_factor(X.values, i) for i in range(X.shape[1])]
    return VIF_data

def delete_multicollinearity(df, target_name, VIF_threshold):
    X = df.drop(target_name, axis=1)
    VIF_mat = VIF_calculation(X)
    while any(VIF_mat["VIF"] > VIF_threshold):
        highest_VIF = VIF_mat[VIF_mat["VIF"] > VIF_threshold].max()
        X = X.drop(highest_VIF["variable"], axis=1)
        VIF_mat = VIF_calculation(X)
    X[target_name] = df[target_name]

    display(VIF_mat)
    return X

final_transaction = delete_multicollinearity(final_transaction, 'isFraud', 10)
variable VIF
0 TransactionDT 2.796712
1 TransactionAmt 1.283763
2 card2 2.768351
3 C3 1.001665
4 C5 1.074234
5 D3 1.194289
6 D4 1.838342
7 D11 1.929782

Luego de realizar el VIF, tenemos las variables con las cuales no presenta multicolinealidad.

Análisis Gráfico de los datos#

Para realizar este análisis se considerarán las siguientes variables: TransactionDT, TransactionAmt, C5, isFraud, card2, M1_F, D11, M6_T, D3

data = final_transaction[['TransactionDT', 'TransactionAmt', 'C5', 'isFraud', 'card2','D11', 'D3']]
variables = ['TransactionAmt', 'C5', 'card2', 'D11', 'D3']
n_cols = 2
n_rows = int(np.ceil(len(variables) / n_cols))

plt.figure(figsize=(n_cols * 8, n_rows * 6))

for i, var in enumerate(variables, 1):
    plt.subplot(n_rows, n_cols, i)
    sns.boxplot(x='isFraud', y=var, data=data)
    plt.title(f'Boxplot of {var} by Fraud Status')

plt.tight_layout()
plt.show()
_images/5429359ae797a308b0f9e0e08db48513ec6c1500f767463a026c994cdd60d3e9.png

Boxplot de TransactionAmt por fraude: Se observa que la mediana de la cantidad de transacción para los casos fraudulentos es más baja que para los casos no fraudulentos. También hay puntos extremos en ambos casos, indicando valores atípicos. La dispersión parece ser mayor en las transacciones no fraudulentas.

Boxplot de C5 por fraude: La variable C5 muestra una cantidad considerable de valores atípicos en ambas categorías de fraude. La distribución parece ser similar entre los dos grupos.

Boxplot de card2 por fraude: Esta parece ser otra variable categórica o una con un número limitado de valores distintos. La distribución es casi idéntica en ambos grupos, con los mismos ‘pisos’ y ‘techos’ del boxplot, lo que sugiere que card2 no varía con respecto al fraude.

Boxplot de D11 por fraude: La variable D11 muestra algunos valores extremos, especialmente en las transacciones no fraudulentas. La mediana y la dispersión parecen ser más grandes en las transacciones no fraudulentas.

Boxplot de D3 por fraude: La variable D3 muestra una gran cantidad de valores atípicos en las transacciones no fraudulentas y una mayor dispersión en comparación con las transacciones fraudulentas.

plt.figure(figsize=(n_cols * 8, n_rows * 6))

for i, var in enumerate(variables, 1):
    plt.subplot(n_rows, n_cols, i)
    sns.scatterplot(x=var, y='isFraud', data=data)
    plt.title(f'Sactterplot of {var} by Fraud Status')

plt.tight_layout()
plt.show()
_images/43861e14df9554d836838d174f13afce25989c25ecbe90bfb8499d6480160b5e.png

Scatterplot de TransactionAmt por fraude: La mayoría de las transacciones, tanto fraudulentas como no fraudulentas, parecen tener montos más bajos. Hay unos pocos valores atípicos con montos más altos en el lado no fraudulento.

Scatterplot de C5 por fraude: Los puntos se concentran en valores bajos para ambas categorías, con pocos valores atípicos. No hay una diferencia distintiva visible entre los estados de fraude.

Scatterplot de card2 por fraude: Los datos parecen distribuirse uniformemente a lo largo de varios valores distintos, con una cantidad similar de puntos para cada estado de fraude.

Scatterplot de D11 por fraude: Los puntos están ampliamente distribuidos, pero no hay una clara distinción entre transacciones fraudulentas y no fraudulentas.

Scatterplot de D3 por fraude: Hay una distribución amplia de puntos para las transacciones no fraudulentas y unos pocos valores atípicos para las fraudulentas. Sin embargo, no hay una separación clara entre los grupos.

Merge de los dos datasets#

Ahora realizaremos un merge de los dos datasets usando la columna TransactionID

final_dataset = pd.merge(final_transaction, identity, on='TransactionID', how='inner')